// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analyzer/dart/analysis/declared_variables.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/context/source.dart';
import 'package:analyzer/src/dart/analysis/file_state.dart';

import 'package:analyzer/src/dart/ast/ast.dart';

import 'package:analyzer/src/dart/element/element.dart';


import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/utilities_dart.dart';
import 'package:analyzer/src/ignore_comments/ignore_info.dart';

import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer/src/util/uri.dart';
import 'package:pub_semver/pub_semver.dart';

var timerLibraryAnalyzer = Stopwatch();

var timerLibraryAnalyzerFreshUnit = Stopwatch();
var timerLibraryAnalyzerResolve = Stopwatch();
var timerLibraryAnalyzerSplicer = Stopwatch();
var timerLibraryAnalyzerVerify = Stopwatch();

class AnalysisForCompletionResult {
  final CompilationUnit parsedUnit;
  final List<AstNode> resolvedNodes;

  AnalysisForCompletionResult({
    required this.parsedUnit,
    required this.resolvedNodes,
  });
}

/// A container with information about used imports prefixes and used imported
/// elements.
class UsedImportedElements {

  /// The set of referenced top-level elements.
  final Set<Element> elements = {};

}
/// Analyzer of a single library.
class LibraryAnalyzer {
  /// A marker object used to prevent the initialization of
  /// [_versionConstraintFromPubspec] when the previous initialization attempt
  /// failed.
  static final VersionRange noSpecifiedRange = VersionRange();
  final AnalysisOptionsImpl _analysisOptions;
  final DeclaredVariables _declaredVariables;
  final SourceFactory _sourceFactory;
  final FileState _library;

  final AnalysisContext _context;

  final LibraryElementImpl _libraryElement;

  final Map<FileState, LineInfo> _fileToLineInfo = {};

  final Map<FileState, IgnoreInfo> _fileToIgnoreInfo = {};
  final Map<FileState, RecordingErrorListener> _errorListeners = {};
  final Map<FileState, ErrorReporter> _errorReporters = {};

  final List<UsedImportedElements> _usedImportedElementsList = [];
  final List<UsedLocalElements> _usedLocalElementsList = [];

  final Set<ConstantEvaluationTarget> _constants = {};

  LibraryAnalyzer(
      this._analysisOptions,
      this._declaredVariables,
      this._sourceFactory,
      this._context,
      this._libraryElement,
      this._library,
      );


  /// Compute analysis results for all units of the library.
  Map<FileState, UnitAnalysisResult> analyze() {
    timerLibraryAnalyzer.start();
    Map<FileState, CompilationUnitImpl> units = {};

    // Parse all files.
    timerLibraryAnalyzerFreshUnit.start();
    for (FileState file in _library.libraryFiles) {
      units[file] = _parse(file);
    }
    timerLibraryAnalyzerFreshUnit.stop();

    // Resolve URIs in directives to corresponding sources.

    units.forEach((file, unit) {
      _resolveUriBasedDirectives(file, unit);
    });

    timerLibraryAnalyzerResolve.start();
    _resolveDirectives(units);

    units.forEach((file, unit) {
      _resolveFile(file, unit);
    });
    timerLibraryAnalyzerResolve.stop();


    timerLibraryAnalyzerVerify.start();
    units.forEach((file, unit) {
      _computeVerifyErrors(file, unit);
    });

    if (_analysisOptions.hint) {
      units.forEach((file, unit) {
        {
          var visitor = GatherUsedLocalElementsVisitor(_libraryElement);
          unit.accept(visitor);
          _usedLocalElementsList.add(visitor.usedElements);
        }
        {
          var visitor = GatherUsedImportedElementsVisitor(_libraryElement);
          unit.accept(visitor);
          _usedImportedElementsList.add(visitor.usedElements);
        }
      });
      units.forEach((file, unit) {
        _computeHints(file, unit);
      });
    }

    _checkForInconsistentLanguageVersionOverride(units);


    timerLibraryAnalyzerVerify.stop();

    // Return full results.
    Map<FileState, UnitAnalysisResult> results = {};
    units.forEach((file, unit) {
      List<AnalysisError> errors = _getErrorListener(file).errors;
      errors = _filterIgnoredErrors(file, errors);
      results[file] = UnitAnalysisResult(file, unit, errors);
    });
    timerLibraryAnalyzer.stop();
    return results;
  }

  AnalysisForCompletionResult analyzeForCompletion({
    required FileState file,
    required int offset,
    required CompilationUnitElementImpl unitElement,
    required OperationPerformanceImpl performance,
  }) {
    var parsedUnit = performance.run('parse', (performance) {
      return _parse(file);
    });
    return AnalysisForCompletionResult(
      parsedUnit: parsedUnit,
      resolvedNodes: [],
    );

  }

  void _checkForInconsistentLanguageVersionOverride(
    Map<FileState, CompilationUnit> units,
  ) {
    var libraryEntry = units.entries.first;
    var libraryUnit = libraryEntry.value;

    var elementToUnit = <CompilationUnitElement, CompilationUnit>{};
    for (var entry in units.entries) {
      var unit = entry.value;
      elementToUnit[unit.declaredElement!] = unit;
    }

  }


  void _computeHints(FileState file, CompilationUnit unit) {
    AnalysisErrorListener errorListener = _getErrorListener(file);
    ErrorReporter errorReporter = _getErrorReporter(file);



    TodoFinder(errorReporter).findIn(unit);
    LanguageVersionOverrideVerifier(errorReporter).verify(unit);

    // Verify imports.
    {
      ImportsVerifier verifier = ImportsVerifier();
      verifier.addImports(unit);
      _usedImportedElementsList.forEach(verifier.removeUsedElements);
      verifier.generateDuplicateImportHints(errorReporter);
      verifier.generateDuplicateShownHiddenNameHints(errorReporter);
      verifier.generateUnusedImportHints(errorReporter);
      verifier.generateUnusedShownNameHints(errorReporter);
      verifier.generateUnnecessaryImportHints(
          errorReporter, _usedImportedElementsList);
    }

    // Unused local elements.
    {
      UsedLocalElements usedElements =
          UsedLocalElements.merge(_usedLocalElementsList);
      UnusedLocalElementsVerifier visitor = UnusedLocalElementsVerifier(
          errorListener, usedElements, _inheritance, _libraryElement);
      unit.accept(visitor);
    }

  }


  void _computeVerifyErrors(FileState file, CompilationUnit unit) {
    ErrorReporter errorReporter = _getErrorReporter(file);

    CodeChecker checker = CodeChecker(
      _typeProvider,
      _typeSystem,
      errorReporter,
    );
    checker.visitCompilationUnit(unit);

    //
    // Validate the directives.
    //
    _validateUriBasedDirectives(file, unit);


    // Use the ErrorVerifier to compute errors.
    //
    ErrorVerifier errorVerifier = ErrorVerifier(
        errorReporter, _libraryElement, _typeProvider, _inheritance);
    unit.accept(errorVerifier);

  }

  /// Return a subset of the given [errors] that are not marked as ignored in
  /// the [file].
  List<AnalysisError> _filterIgnoredErrors(
      FileState file, List<AnalysisError> errors) {
    if (errors.isEmpty) {
      return errors;
    }

    IgnoreInfo ignoreInfo = _fileToIgnoreInfo[file]!;
    if (!ignoreInfo.hasIgnores) {
      return errors;
    }

    LineInfo lineInfo = _fileToLineInfo[file]!;

    var unignorableCodes = _analysisOptions.unignorableNames;

    bool isIgnored(AnalysisError error) {
      var code = error.errorCode;
      // Don't allow un-ignorable codes to be ignored.
      if (unignorableCodes.contains(code.name) ||
          unignorableCodes.contains(code.uniqueName) ||
          // Lint rules have lower case names.
          unignorableCodes.contains(code.name.toUpperCase())) {
        return false;
      }
      int errorLine = lineInfo.getLocation(error.offset).lineNumber;
      return ignoreInfo.ignoredAt(code, errorLine);
    }

    return errors.where((AnalysisError e) => !isIgnored(e)).toList();
  }

  /// Find constants to compute.
  void _findConstants(CompilationUnit unit) {

  }

  RecordingErrorListener _getErrorListener(FileState file) =>
      _errorListeners.putIfAbsent(file, () => RecordingErrorListener());

  ErrorReporter _getErrorReporter(FileState file) {
    return _errorReporters.putIfAbsent(file, () {
      RecordingErrorListener listener = _getErrorListener(file);
      return ErrorReporter(
        listener,
        file.source,
        isNonNullableByDefault: _libraryElement.isNonNullableByDefault,
      );
    });
  }

  bool _isExistingSource(Source source) {
    if (source is InSummarySource) {
      return true;
    }
    for (var file in _library.directReferencedFiles) {
      if (file.uri == source.uri) {
        return file.exists;
      }
    }
    // A library can refer to itself with an empty URI.
    return source == _library.source;
  }

  /// Return a new parsed unresolved [CompilationUnit].
  CompilationUnitImpl _parse(FileState file) {
    AnalysisErrorListener errorListener = _getErrorListener(file);
    String content = file.content;
    var unit = file.parse(errorListener);

    LineInfo lineInfo = unit.lineInfo!;
    _fileToLineInfo[file] = lineInfo;
    _fileToIgnoreInfo[file] = IgnoreInfo.forDart(unit, content);

    return unit;
  }

  void _resolveDirectives(Map<FileState, CompilationUnitImpl> units) {
    var definingCompilationUnit = units[_library]!;
    definingCompilationUnit.element = _libraryElement.definingCompilationUnit;

    bool matchNodeElement(Directive node, Element element) {
      return node.keyword.offset == element.nameOffset;
    }

    ErrorReporter libraryErrorReporter = _getErrorReporter(_library);

    LibraryIdentifier? libraryNameNode;
    var seenPartSources = <Source>{};
    var directivesToResolve = <DirectiveImpl>[];
    int partDirectiveIndex = 0;
    int partElementIndex = 0;
    for (Directive directive in definingCompilationUnit.directives) {
      if (directive is LibraryDirectiveImpl) {
        libraryNameNode = directive.name;
        directivesToResolve.add(directive);
      } else if (directive is ImportDirectiveImpl) {
        for (ImportElement importElement in _libraryElement.imports) {
          if (matchNodeElement(directive, importElement)) {
            directive.element = importElement;
            directive.prefix?.staticElement = importElement.prefix;
            var importedLibrary = importElement.importedLibrary;
            if (importedLibrary is LibraryElementImpl) {
              if (importedLibrary.hasPartOfDirective) {
                // It is safe to assume that `directive.uri.stringValue` is
                // non-`null`, because the only time it is `null` is if the URI
                // contains a string interpolation, in which case the import
                // would never have resolved in the first place.
                libraryErrorReporter.reportErrorForNode(
                    CompileTimeErrorCode.IMPORT_OF_NON_LIBRARY,
                    directive.uri,
                    [directive.uri.stringValue!]);
              }
            }
          }
        }
      } else if (directive is ExportDirectiveImpl) {
        for (ExportElement exportElement in _libraryElement.exports) {
          if (matchNodeElement(directive, exportElement)) {
            directive.element = exportElement;
            var exportedLibrary = exportElement.exportedLibrary;
            if (exportedLibrary is LibraryElementImpl) {
              if (exportedLibrary.hasPartOfDirective) {
                // It is safe to assume that `directive.uri.stringValue` is
                // non-`null`, because the only time it is `null` is if the URI
                // contains a string interpolation, in which case the export
                // would never have resolved in the first place.
                libraryErrorReporter.reportErrorForNode(
                    CompileTimeErrorCode.EXPORT_OF_NON_LIBRARY,
                    directive.uri,
                    [directive.uri.stringValue!]);
              }
            }
          }
        }
      }
    }

    // TODO(brianwilkerson) Report the error
    // ResolverErrorCode.MISSING_LIBRARY_DIRECTIVE_WITH_PART

    //
    // Resolve the relevant directives to the library element.
    //
    for (var directive in directivesToResolve) {
      directive.element = _libraryElement;
    }

    // TODO(scheglov) remove DirectiveResolver class
  }

  void _resolveFile(FileState file, CompilationUnit unit) {
    var source = file.source;
    RecordingErrorListener errorListener = _getErrorListener(file);

    var unitElement = unit.declaredElement as CompilationUnitElementImpl;
  }

  Uri? _resolveRelativeUri(String relativeUriStr) {
    Uri relativeUri;
    try {
      relativeUri = Uri.parse(relativeUriStr);
    } on FormatException {
      return null;
    }

    var absoluteUri = resolveRelativeUri(_library.uri, relativeUri);
    return rewriteToCanonicalUri(_sourceFactory, absoluteUri);
  }

  /// Return the result of resolve the given [uriContent], reporting errors
  /// against the [uriLiteral].
  Source? _resolveUri(FileState file, bool isImport, StringLiteral uriLiteral,
      String? uriContent) {
    UriValidationCode? code =
        UriBasedDirectiveImpl.validateUri(isImport, uriLiteral, uriContent);
    if (code == null) {
      try {
        Uri.parse(uriContent!);
      } on FormatException {
        return null;
      }
      return _sourceFactory.resolveUri(file.source, uriContent);
    } else if (code == UriValidationCode.URI_WITH_INTERPOLATION) {
      _getErrorReporter(file).reportErrorForNode(
          CompileTimeErrorCode.URI_WITH_INTERPOLATION, uriLiteral);
      return null;
    } else if (code == UriValidationCode.INVALID_URI) {
      // It is safe to assume [uriContent] is non-null because the only way for
      // it to be null is if the string literal contained an interpolation, and
      // in that case the validation code would have been
      // UriValidationCode.URI_WITH_INTERPOLATION.
      assert(uriContent != null);
      _getErrorReporter(file).reportErrorForNode(
          CompileTimeErrorCode.INVALID_URI, uriLiteral, [uriContent!]);
      return null;
    }
    return null;
  }

  void _resolveUriBasedDirectives(FileState file, CompilationUnit unit) {
    for (var directive in unit.directives) {
      if (directive is UriBasedDirectiveImpl) {
        StringLiteral uriLiteral = directive.uri;
        String? uriContent = uriLiteral.stringValue?.trim();
        directive.uriContent = uriContent;
        Source? defaultSource = _resolveUri(
            file, directive is ImportDirective, uriLiteral, uriContent);
        directive.uriSource = defaultSource;
      }
      if (directive is NamespaceDirectiveImpl) {
        var relativeUriStr = _selectRelativeUri(directive);
        directive.selectedUriContent = relativeUriStr;
        var absoluteUri = _resolveRelativeUri(relativeUriStr);
        if (absoluteUri != null) {
          directive.selectedSource = _sourceFactory.forUri2(absoluteUri);
        }
        for (var configuration in directive.configurations) {
          configuration as ConfigurationImpl;
          var uriLiteral = configuration.uri;
          String? uriContent = uriLiteral.stringValue?.trim();
          Source? defaultSource = _resolveUri(
              file, directive is ImportDirective, uriLiteral, uriContent);
          configuration.uriSource = defaultSource;
        }
      }
    }
  }

  String _selectRelativeUri(NamespaceDirective directive) {
    for (var configuration in directive.configurations) {
      var name = configuration.name.components.join('.');
      var value = configuration.value?.stringValue ?? 'true';
      if (_declaredVariables.get(name) == value) {
        return configuration.uri.stringValue ?? '';
      }
    }
    return directive.uri.stringValue ?? '';
  }

  /// Validate that the feature set associated with the compilation [unit] is
  /// the same as the [expectedSet] of features supported by the library.
  void _validateFeatureSet(CompilationUnit unit, FeatureSet expectedSet) {
    FeatureSet actualSet = unit.featureSet;
    if (actualSet != expectedSet) {
      // TODO(brianwilkerson) Generate a diagnostic.
    }
  }

  /// Check the given [directive] to see if the referenced source exists and
  /// report an error if it does not.
  void _validateUriBasedDirective(
      FileState file, UriBasedDirectiveImpl directive) {
    String? uriContent;
    Source? source;
    if (directive is NamespaceDirectiveImpl) {
      uriContent = directive.selectedUriContent;
      source = directive.selectedSource;
    } else {
      uriContent = directive.uriContent;
      source = directive.uriSource;
    }
    if (source != null) {
      if (_isExistingSource(source)) {
        return;
      }
    } else {
      // Don't report errors already reported by ParseDartTask.resolveDirective
      // TODO(scheglov) we don't use this task here
      if (directive.validate() != null) {
        return;
      }
    }

    if (uriContent != null && uriContent.startsWith('dart-ext:')) {
      _getErrorReporter(file).reportErrorForNode(
        CompileTimeErrorCode.USE_OF_NATIVE_EXTENSION,
        directive.uri,
      );
      return;
    }

    CompileTimeErrorCode errorCode = CompileTimeErrorCode.URI_DOES_NOT_EXIST;
    if (isGeneratedSource(source)) {
      errorCode = CompileTimeErrorCode.URI_HAS_NOT_BEEN_GENERATED;
    }
    // It is safe to assume that [uriContent] is non-null because the only way
    // for it to be null is if the string literal contained an interpolation,
    // and in that case the call to `directive.validate()` above would have
    // returned a non-null validation code.
    _getErrorReporter(file)
        .reportErrorForNode(errorCode, directive.uri, [uriContent!]);
  }

  /// Check each directive in the given [unit] to see if the referenced source
  /// exists and report an error if it does not.
  void _validateUriBasedDirectives(FileState file, CompilationUnit unit) {
    for (Directive directive in unit.directives) {
      if (directive is UriBasedDirectiveImpl) {
        _validateUriBasedDirective(file, directive);
      }
    }
  }

}

/// Analysis result for single file.
class UnitAnalysisResult {
  final FileState file;
  final CompilationUnit unit;
  final List<AnalysisError> errors;

  UnitAnalysisResult(this.file, this.unit, this.errors);
}

/// Either the name or the source associated with a part-of directive.
class _NameOrSource {
  final String? name;
  final Source? source;

  _NameOrSource(this.name, this.source);
}
